互斥对象和锁

互斥对象的主要操作有两个,即加锁(lock)和解锁(unlock)。当一个线程对互斥对象进行lock操作并成功获得这个互斥对象的所有权,在此线程对此对象unlock前,其他线程对这个互斥对象的lock操作都会被阻塞。

多个互斥对象加锁

有些倾向需要对多个互斥对象进行加锁,考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
std::mutex mt1, mt2;
{
std::lock_guard<std::mutex> lock1(mt1);
std::lock_guard<std::mutex> lock2(mt2);
}
{
std::lock_guard<std::mutex> lock1(mt2);
std::lock_guard<std::mutex> lock2(mt1);
}

上面程序中,如果线程1执行到第4行的时候恰好线程2执行到第8行,那么就会出现下面的情况:

  • 线程1持有mt1,等待mt2
  • 线程2持有mt2,等待mt1

线程1和线程2持有各自的mutex,互不松手,导致两个线程谁也运行不了,造成死锁现象。

通常情况,要含有多个互斥对象时,为了避免死锁,要求在多个线程中进行加锁时应保证其先后顺序一致,如下:

1
2
3
4
5
6
7
8
9
10
std::mutex mt1, mt2;
{
std::lock_guard<std::mutex> lock1(mt1);
std::lock_guard<std::mutex> lock2(mt2);
}
{
std::lock_guard<std::mutex> lock1(mt1);
std::lock_guard<std::mutex> lock2(mt2);
}

这样的话,即使线程1执行到第4行的时候线程2执行到第8行,由于互斥对象mt1已经被上锁,线程2只能等待线程1释放mt1,因此也就不会出现前面例子所说的死锁现象。

其实更好的做法是使用标准库中的std::lock和std::try_lock函数对多个互斥对象进行加锁,std::lock()可以对多个互斥对象一次性进行加锁。std::lock会使用一种避免死锁的算法对多个待加锁对象进行lock操作,当待加锁的对象中有不可用对象时std::lock会阻塞当前线程直到所有对象都可用。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
std::mutex mt1, mt2;
{
std::unique_lock<std::mutex> lock1(mt1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mt2, std::defer_lock);
std::lock(lck1, lck2);
}
{
std::unique_lock<std::mutex> lock1(mt1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mt2, std::defer_lock);
std::lock(lck2, lck1);
}